Skip to content

嵌入式C - 按键驱动,支持连击、长按、组合键

image-20230915210121586

构想的形态

初步梳理需求,并预测未来的场景,构建如下形态:

image-20230915210236690

  • 它要识别出独立按键的不同操作,即事件
  • 自动完成按键的状态转移
  • 也需要有组合键功能
  • 必须处理好优先级,不能说三击触发了,双击也能触发

总的要求

  • 对应低性能的MCU,实现有限的按键识别
  • 具备组合键功能
  • 状态与事件处理分离
  • 使用便捷,移植性高

按键结构设计

考虑到按键的管理,使用单向链表串起所有按键:

  • 独立按键链表
  • 组合键链表

尽可能让二者前半部分保持一致,如下:

image-20230915210402636

image-20230915210423894

独立按键扫描的数据结构

主要有电平、滤波、连击部分、阈值部分,满足扫描需求:

c
typedef struct
{
  uint8_t    level       : 1;    /* 当前电平 */
  uint8_t    valid_level : 1;    /* 有效电平,按键激活判断 */
  uint8_t    reserve     : 2;    /* 保留 */
  uint8_t    click_cnt   : 4;    /* 连击次数 */
  uint8_t    filter_cnt;         /* 电平滤波计数 */
  uint8_t    filter_limit;       /* 电平滤波阈值 */
  uint16_t   tick;               /* 按键扫描计时 */
  uint16_t   double_click_limit; /* 双击时间的阈值 */
  uint16_t   long_limit;         /* 长按的阈值 */
  uint16_t   long_long_limit;    /* 超长按的阈值 */
  uint8_t  (*get_level)(void);   /* 电平获取,函数指针 */
}key_scan_t;

部分按键初始化配置的API

  • 按键初始化
  • 事件回调函数注册
  • 组合键关联独立按键
c
/* 注册一个独立按键 */
void ry_key_reg(ry_key_t *key,
    uint8_t valid_level,        /* 有效电平,按键激活判断 */
    uint8_t filter,             /* 电平滤波阈值 */
    uint8_t double_click_limit, /* 双击时间的阈值 */
    uint8_t long_limit,         /* 长按的阈值 */
    uint8_t long_long_limit,    /* 超长按的阈值 */
    uint8_t (*get_level)(void))
/* 组合键的注册函数 */
void ry_key_compound_reg(ry_key_compound_t *key, callback cbk)
/* 组合键添加关联的独立按键SN号,以SN大小插入 */
void ry_key_compound_insert_key_sn(ry_key_compound_t *key, uint8_t sn)
/* 注册按键的回调函数 */
#define RY_KEY_CALLBACK_CFG(key, event, cbk)

独立按键的状态机

总的结构:

image-20230915210638914

代码实现:

c
uint8_t ry_key_state_machine(ry_key_t *key)
{
  __key_level_scan(key);
  key->scan.tick++;

  switch(key->status)
  {
  case KEY_IDLE_STATUS :
    if(key->scan.level == key->scan.valid_level)
    {
      __key_event_mark(key, KEY_DOWN_EVENT);
      key->scan.tick      = 0;
      key->scan.click_cnt = 0;
    }
    break;
  case KEY_DOWN_STATUS :
    if(key->scan.level != key->scan.valid_level)
    {
      __key_event_mark(key, KEY_UP_EVENT);
      key->scan.click_cnt++;
    }
    else if(key->scan.tick > key->scan.long_limit)
      __key_event_mark(key, KEY_LONG_PRESS_EVENT);
    break;
  case KEY_UP_STATUS :
    if(key->scan.level == key->scan.valid_level)
    {
      __key_event_mark(key, KEY_DOWN_STATUS);
      key->scan.tick = 0;
    }
    /* 从按键按下开始计时,若时间超过连击时间阈值,则认为连击结束 */
    else if(key->scan.tick > key->scan.double_click_limit)
    {
      if(1 == key->scan.click_cnt)
        __key_event_mark(key, KEY_SINGLE_CLICK_EVENT);
      else if(2 == key->scan.click_cnt)
        __key_event_mark(key, KEY_DOUBLE_CLICK_EVENT);
      else if(3 == key->scan.click_cnt)
        __key_event_mark(key, KEY_THREE_CLICK_EVENT);
      else
        key->status = KEY_IDLE_STATUS;
    }
    break;
  case KEY_LONG_PRESS_STATUS :
    if(key->scan.tick > key->scan.long_long_limit)
      __key_event_mark(key, KEY_LONG_LONG_PRESS_EVENT);
    else if(key->scan.level != key->scan.valid_level)
      key->status     = KEY_IDLE_STATUS;
    break;
  default :
    key->status         = KEY_IDLE_STATUS;
    break;
  }
  return key->event;
}

按键扫描的机制

  • 先扫描所有按键,再执行事件处理
  • 某个时刻,只允许一个事件,多了全部无效

image-20230915210736537

使用场景

场景说明:

  • 一个电源按键,长按
  • 功能按键,单击
  • 组合键为“电源键” + “功能键”

定义几个按键,并准备电平读取函数:

c
static ry_key_t __keyPower;              /* 电源按键 */
static ry_key_t __keyCtr;                /* 控制按键 */
static ry_key_compound_t __compoundKey1; /* 组合键 */

extern uint8_t key_power_get_level(void);
extern uint8_t key_ctr_get_level(void);

初始化按键

初始化如下:

c
void user_key_init(void)
{
  ry_key_reg(&__keyPower, 1, 5, 50, 300, 900, key_power_get_level);
  ry_key_reg(&__keyCtr,   1, 5, 50, 300, 900, key_ctr_get_level);
  ry_key_compound_reg(&__compoundKey1, compound_key1_callback);
  /* 配置事件的回调函数 */
  RY_KEY_CALLBACK_CFG(__keyPower, KEY_LONG_PRESS_EVENT, key_power_long_press_callback);
  RY_KEY_CALLBACK_CFG(__keyCtr, KEY_SINGLE_CLICK_EVENT, key_ctr_single_click_callback);
  /* 组合键关联对应的独立按键 */
  ry_key_compound_insert_key_sn(&__compoundKey1, __keyPower.sn);
  ry_key_compound_insert_key_sn(&__compoundKey1, __keyCtr.sn);
}

准备几个回调函数:

c
void key_power_long_press_callback(ry_key_t *key)
{
  printf("key_power_long_press_callback");
}
void key_ctr_single_click_callback(ry_key_t *key)
{
  printf("key_ctr_single_click_callback");
}
void compound_key1_callback(ry_key_t *key)
{
  printf("compound_key1_callback");
}

主函数

这样定时扫描按键,就会自动运行我们定义的事件回调函数。

c
int main(void)
{
  //system_init();
  user_key_init();
  while(1)
  {
    /* 配置定时器,定时扫描按键效果更好 */
    ry_key_scan();
  }
}

————————————————

版权

版权声明:本文为「碎片聚合」的原创文章。

原文链接:https://mp.weixin.qq.com/s/WOC8pWIC3NPuSRs5MurvWQ

上次更新于: